package com.ds.lens.data.service;

import com.alibaba.fastjson.JSONObject;
import com.ds.lens.data.common.apmapi.ApmMetricClient;
import com.ds.lens.data.common.apmapi.in.Condition;
import com.ds.lens.data.common.apmapi.in.Metrics;
import com.ds.lens.data.common.apmapi.in.Range;
import com.ds.lens.data.common.apmapi.in.SimpleApmMetricInVO;
import com.ds.lens.data.common.apmapi.out.ApmMetricOutVO;
import com.ds.lens.data.common.asserts.Asserts;
import com.ds.lens.data.common.client.vo.LensMetricInVO;
import com.ds.lens.data.common.constant.AggregatorEnum;
import com.ds.lens.data.common.constant.Constants;
import com.ds.lens.data.common.util.HttpServiceClient;
import com.ds.lens.data.service.dao.LensMetricDao;
import com.ds.lens.data.service.esservice.MetricsOperateService;
import com.ds.lens.data.service.metric.MetricQueryFactory;
import com.ds.lens.data.service.metric.MetricQueryService;
import com.ds.lens.data.vo.out.v2.integration.LensIntegrationDetailOutVO;
import com.ds.lens.data.vo.out.v2.metric.LensMetricNamesOutVO;
import com.ds.lens.data.vo.out.v2.monitor.LensMonitorFilterOutVO;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.histogram.ExtendedBounds;
import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;

/**
 * @author WeiShaoying
 * @date 2019/11/8 下午2:43
 */
@Slf4j
@Service
public class LensMetricService {
    private static final String APM = "APM";

    private static final String METRICS = "metrics";

    private static int DEFAULT_SIZE = 10000;

    @Value("${lens.APM.website.host}")
    private String apmWebsiteHost;

    @Autowired
    LensIntegrationService lensIntegrationService;

    @Autowired
    LensMetricDao lensMetricDao;

    @Autowired
    LensMetricsTagsService lensMetricsTagsService;

    @Autowired
    MetricsOperateService metricsOperateService;

    @Autowired
    MetricQueryFactory metricQueryFactory;

    public List<Map<String, Object>> queryMetric(LensMetricInVO lensMetricInVO) {
        LensIntegrationDetailOutVO integrationDetailOutVO = lensIntegrationService
                .findLensIntegrationsById(lensMetricInVO.getIntegrationId());
        if (APM.equals(integrationDetailOutVO.getName())) {
            return processAPM(lensMetricInVO, integrationDetailOutVO.getUrl());
        } else if (METRICS.equals(integrationDetailOutVO.getName())) {
            return processMetrics(lensMetricInVO);
        }
        return null;
    }

    private List<Map<String, Object>> processAPM(LensMetricInVO lensMetricInVO, String url) {
        SimpleApmMetricInVO.SimpleApmMetricInVOBuilder requestDataBuilder = SimpleApmMetricInVO.builder();
        ApmMetricClient metricClient = HttpServiceClient.getRestClient(ApmMetricClient.class, url);
        // range
        requestDataBuilder.range(new Range(lensMetricInVO.getStartTime(), lensMetricInVO.getEndTime()));
        // conditions
        Asserts.assertNotEmpty(lensMetricInVO.getScope(), Constants.ErrorCode.PARAM_ERROR);

        List<Condition> conditions = new ArrayList<>();
        lensMetricInVO.getScope().forEach((s, o) -> {
            List<String> list = new ArrayList<>();
            if (o instanceof Collection) {
                list.addAll((Collection<? extends String>) o);
            } else {
                list.add(o.toString());
            }
            conditions.add(new Condition<>(s, list));
        });
        // metrics
        Metrics metrics = new Metrics();
        switch (AggregatorEnum.valueOf(lensMetricInVO.getMetricAggr())) {
            case SUM:
                metrics.setSum(lensMetricInVO.getMetricName());
                break;
            case AVG:
                metrics.setAvg(lensMetricInVO.getMetricName());
                break;
            case MAX:
                metrics.setMax(lensMetricInVO.getMetricName());
                break;
            case MIN:
                metrics.setMin(lensMetricInVO.getMetricName());
                break;
            case COUNT:
                metrics.setCount(lensMetricInVO.getMetricName());
                break;
        }
        metrics.setLayer("tierRequest");
        requestDataBuilder.metrics(metrics);
        if (!CollectionUtils.isEmpty(lensMetricInVO.getMetricFilter())) {
            lensMetricInVO.getMetricFilter().forEach((s, o) -> {
                conditions.add(new Condition<>(s, Collections.singletonList(o)));
                if (s.startsWith("instance")) {
                    metrics.setLayer("instanceRequest");
                }
            });
        }
        requestDataBuilder.condition(conditions);
        if (!CollectionUtils.isEmpty(lensMetricInVO.getMetricBy())) {
            requestDataBuilder.group(lensMetricInVO.getMetricBy());
        }
        // interval
        requestDataBuilder.interval(lensMetricInVO.getInterval() * 60000);
        SimpleApmMetricInVO requestData = requestDataBuilder.build();
        log.info("Request APM :" + JSONObject.toJSONString(requestData));
        ApmMetricOutVO<List<Map<String, Object>>> result;
        try {
            result = metricClient.getMetricMetadataFromSimple(requestData);
            if (result.getCode() == 0) {
                return result.getData();
            }
            log.info("Response from APM :" + JSONObject.toJSONString(result));
        } catch (Exception e) {
            log.error("Error acquiring data from APM.", e);
        }
        return null;
    }


    private List<Map<String, Object>> processMetrics(LensMetricInVO lensMetricInVO) {
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        boolQuery.filter(QueryBuilders.rangeQuery("timeStamp").gte(lensMetricInVO.getStartTime()).lt(lensMetricInVO.getEndTime()));
        boolQuery.filter(QueryBuilders.termQuery("metricName", lensMetricInVO.getMetricName()));
        boolQuery.filter(QueryBuilders.termQuery("appId", lensMetricInVO.getAppId()));
        if (!CollectionUtils.isEmpty(lensMetricInVO.getMetricFilter())) {
            lensMetricInVO.getMetricFilter().forEach((s, o) -> {
                boolQuery.filter(QueryBuilders.termQuery("tags." + s, o));
            });
        }
        nativeSearchQueryBuilder.withQuery(boolQuery);
        AbstractAggregationBuilder value = null;
        switch (AggregatorEnum.valueOf(lensMetricInVO.getMetricAggr())) {
            case SUM:
                value = AggregationBuilders.sum("value").field("value");
                break;
            case AVG:
                value = AggregationBuilders.avg("value").field("value");
                break;
            case MAX:
                value = AggregationBuilders.max("value").field("value");
                break;
            case MIN:
                value = AggregationBuilders.min("value").field("value");
                break;
            case COUNT:
                value = AggregationBuilders.count("value").field("value");
                break;
        }
        DateHistogramAggregationBuilder date = AggregationBuilders.dateHistogram("time").field("timeStamp")
                .interval(lensMetricInVO.getInterval())
                .extendedBounds(
                        new ExtendedBounds(lensMetricInVO.getStartTime(), lensMetricInVO.getEndTime() - 1))
                .minDocCount(0)
                .subAggregation(value);
        if (!CollectionUtils.isEmpty(lensMetricInVO.getMetricBy())) {
            nativeSearchQueryBuilder.addAggregation(processMetricsGroup(lensMetricInVO.getMetricBy().get(0), lensMetricInVO.getMetricBy(), 0, date));
        } else {
            nativeSearchQueryBuilder.addAggregation(date);
        }
        AggregatedPage<com.ds.lens.data.common.document.Metrics> aggregatedPage = metricsOperateService
                .searchAggregation(nativeSearchQueryBuilder.build());
        if (aggregatedPage.getTotalElements() > 0) {
            return resultMetricsData(lensMetricInVO, aggregatedPage.getAggregations());
        }
        return null;
    }


    private TermsAggregationBuilder processMetricsGroup(String key, List<String> metricBy, int step, DateHistogramAggregationBuilder date) {
        TermsAggregationBuilder terms = AggregationBuilders.terms(key).field("tags." + key).size(DEFAULT_SIZE).shardSize(DEFAULT_SIZE);
        int nextStep = step + 1;
        if (nextStep < metricBy.size()) {
            terms.subAggregation(processMetricsGroup(metricBy.get(nextStep), metricBy, nextStep, date));
        } else {
            terms.subAggregation(date);
        }
        return terms;
    }

    private List<Map<String, Object>> resultMetricsData(LensMetricInVO lensMetricInVO, Aggregations aggregations) {
        String metricName = lensMetricInVO.getMetricName() + "-" + AggregatorEnum.valueOf(lensMetricInVO.getMetricAggr()).name();
        if (!CollectionUtils.isEmpty(lensMetricInVO.getMetricBy())) {
            String key = lensMetricInVO.getMetricBy().get(0);
            return unbindMetricsGroup(metricName, key,
                    lensMetricInVO.getMetricBy(), 0, aggregations.get(key), new HashMap<>());
        } else {
            return unbindMetricsTimeValue(metricName, aggregations.get("time"), new HashMap<>());
        }
    }

    private List<Map<String, Object>> unbindMetricsGroup(String metricName, String key, List<String> metricBy,
                                                         int step, Terms terms, Map<String, String> byValue) {
        List<Map<String, Object>> result = new ArrayList<>();
        int nextStep = step + 1;
        if (nextStep < metricBy.size()) {
            String nextKey = metricBy.get(nextStep);
            for (Terms.Bucket bucket : terms.getBuckets()) {
                byValue.put(key, bucket.getKeyAsString());
                if (bucket.getAggregations().get(nextKey) != null) {
                    result.addAll(unbindMetricsGroup(metricName, nextKey, metricBy, nextStep, bucket.getAggregations().get(nextKey), byValue));
                }
            }
        } else {
            terms.getBuckets().forEach(bucket -> {
                byValue.put(key, bucket.getKeyAsString());
                result.addAll(unbindMetricsTimeValue(metricName, bucket.getAggregations().get("time"), byValue));
            });
        }
        return result;
    }

    private List<Map<String, Object>> unbindMetricsTimeValue(String metricName, Histogram time, Map<String, String> byValue) {
        List<Map<String, Object>> result = new ArrayList<>();
        time.getBuckets().forEach(bucket -> {
            Map<String, Object> map = new HashMap<>();
            map.put(metricName, getValue(bucket.getAggregations().get("value")));
            map.put("timeStamp", bucket.getKeyAsString());
            if (!CollectionUtils.isEmpty(byValue)) {
                map.putAll(byValue);
            }
            result.add(map);
        });
        return result;
    }

    public List<LensMetricNamesOutVO> queryMetricNamesByIntegrationId(long integrationId, long applicationId) {
        LensIntegrationDetailOutVO integrationDetailOutVO = lensIntegrationService.findLensIntegrationsById(integrationId);
        Asserts.assertNotNull(integrationDetailOutVO, Constants.ErrorCode.DATA_NOT_EXIST);

        //Get MetricQueryService instance
        MetricQueryService metricQueryService = metricQueryFactory.getMetricQueryService(Constants.METRIC_SERVICE_PREFIX + integrationDetailOutVO.getName());
        //Call the getGroupByFields method
        return metricQueryService.getMetricNames(integrationDetailOutVO, applicationId);
    }

    public List<LensMonitorFilterOutVO> queryFilters(Long integrationId, Long applicationId, String tierId, String metricName) {
        Asserts.assertNotNull(integrationId, Constants.ErrorCode.PARAM_ERROR, "IntegrationId can not be null.");

        LensIntegrationDetailOutVO integrationDetailOutVO = lensIntegrationService.findLensIntegrationsById(integrationId);
        Asserts.assertNotNull(integrationDetailOutVO, Constants.ErrorCode.DATA_NOT_EXIST);

        //Get MetricQueryService instance
        MetricQueryService metricQueryService = metricQueryFactory.getMetricQueryService(Constants.METRIC_SERVICE_PREFIX + integrationDetailOutVO.getName());
        //Call the getGroupByFields method
        return metricQueryService.getFilters(integrationDetailOutVO, applicationId, tierId, metricName);
    }

    private static double getValue(NumericMetricsAggregation.SingleValue value) {
        if (null == value || Double.isNaN(value.value()) || Double.isInfinite(value.value())) {
            return 0D;
        } else {
            return value.value();
        }
    }
}
